Skip to content

feat(brand-protocol): ratify distributed brand.json + fold + typed trademarks#4505

Merged
bokelley merged 13 commits into
mainfrom
bokelley/impl-3764-3409
May 14, 2026
Merged

feat(brand-protocol): ratify distributed brand.json + fold + typed trademarks#4505
bokelley merged 13 commits into
mainfrom
bokelley/impl-3764-3409

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Consolidates the distributed-brand-json work into one normative PR.

Closes #3409, #3533, #3764, #3910, #3909.

What this lands

1. The RFC, as the normative spec (#3409, #3533, #3764, #3910)

  • brand.json gains a fifth variant — Brand Canonical Document (self-published per-brand document with optional house_domain pointer up).
  • House Portfolio gains brand_refs[] (pointer brands, child-owned canonical docs) alongside brands[] (inline, parent-owned).
  • New managed_by on brand_refs[] entries — house-declared, non-trust-bearing, for grouping/discovery.
  • New #/definitions/brand_ref (extracted for SDK codegen).
  • Required widened from ["house", "brands"] to ["house"] with anyOf requiring at least one of brands[] / brand_refs[].
  • Trust model: mutual-assertion (child's house_domain ↔ house's brand_refs[]). Standalone trumps any third-party claim. Identity is brand-wins; compliance/governance is strictest-of.
  • RFC content (Motivation, Conformance, strictest-of resolution, Prior art) folded into the normative docs/brand-protocol/brand-json.mdx. The standalone proposals/distributed-brand-json-rfc.mdx file is deleted.

2. Typed brand-level trademarks (#3909)

  • New #/definitions/trademark{registry, number, mark} plus optional status, license_type, countries.
  • #/definitions/brand gains trademarks: Trademark[] so inline brands AND Brand Canonical Documents can typed-publish their own marks.
  • House Portfolio's existing inline trademarks[] migrated to $ref — backwards compatible, validates the same shape.
  • Resolution between house-level (corporate) and brand-level (brand-specific) marks is union.

Compatibility

All additions are optional. The existing four variants are unchanged. Existing house-portfolio publishers using untyped trademarks continue to validate against the now-typed Trademark definition (same required fields). No migration required.

Validation

  • npm run build:schemas clean
  • npm run test:schemas 7/7
  • npm run test:examples 36/36
  • npm run test:json-schema 260/260
  • npm run test:composed 40/40
  • npm run typecheck clean
  • npm run check:owned-links clean
  • npm run lint:schema-links clean

Replaces

This PR replaces the draft #3764 (schema cut) and #3533 (RFC docs PR) — both can close once this lands. #3910 and #3909 are addressed inline.

🤖 Generated with Claude Code

bokelley and others added 11 commits May 13, 2026 19:24
Draft RFC proposing per-brand canonical brand.json documents linked by
mutual-assertion pointers, replacing the monolithic-with-inline-children
shape. Hosting (static / CDN / brand-agent / AAO / self) stays an
implementation choice independent of the data model.

Key proposed changes (subject to discussion):
- Each brand publishes one canonical brand.json owning its own attributes
- New `house` pointer for declaring the immediate parent (multi-level
  chains via recursion)
- New `brand_refs[]` (pointer-only) replacing inline `brands[]` content
- New `house_attributes` block for inheritable house-wide metadata
- Mutual-assertion as the trust primitive — child's `house` must be
  reciprocated by parent's `brand_refs[]`

Migration path: 3.x accepts both shapes with deprecation warnings;
brand-protocol 2.0 (decoupled from AdCP major) cuts over.

Lives at docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx
under a new "Proposals" subgroup. Not yet normative — needs spec-owner
sign-off before any code or schema changes land.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RFC stays at docs/brand-protocol/proposals/distributed-brand-json-rfc.mdx
as a working document, but isn't surfaced in the published docs nav.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ouse rename

Addresses Pawel's review on PR #3533:

1. Lead with the operational pain point — Converse can't update its own
   logo in AdCP without editing Nike, Inc.'s file. Reframed Motivation.
2. Hybrid model — brands[] (inline, parent-owned) and brand_refs[]
   (pointer, child-owned) both first-class. Pull-based migration:
   brands[] is no longer deprecated. Sub-brands without their own domain
   stay inline.
3. Flat hierarchy — only the house declares ownership via brand_refs[]
   or brands[]. A brand cannot have its own brand_refs[]. Trust collapses
   to a single hop; no recursive walking.
4. Renamed child-side pointer from `house` to `parent_house` to avoid
   collision with the existing `house` declaration object on the root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed example

check:owned-links treated the example URL as a real link and failed CI
when the path 404'd. Wrap the brand-domain segment as \${domain} so the
linter recognizes it as a placeholder (it skips URLs containing \${).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_domain; add managed_by

Resolves the second round of expert review on PR #3533.

Substantive changes:
- Drop house_attributes / house_attributes_overrides / house_attributes_locked
  entirely. Inheritance/override semantics turned out to be muddy: if a brand
  could weaken a house policy, it wasn't really a house policy. House-level
  fields (data_subject_contestation, trademarks, authorized_operators) are
  already on the house schema; consumers walk house_domain to read them.
  Brand-level additions are just brand-level fields.
- Rename child-side pointer from parent_house ({domain}) to house_domain
  (string). Reuses the existing #/definitions/domain pattern. Drops the
  proposed core/house-ref.json file. Matches the existing House Redirect's
  string-domain convention.
- Add managed_by (string, optional) on brand_refs[] entries. House-declared
  delegation for grouping/discovery. Non-trust-bearing. Captures WPP/Publicis
  reality without reintroducing recursive trust.
- Make house_domain optional on the Brand Canonical Document so standalone
  brands (Patagonia, Liquid Death) have a valid shape. Acquisition adds
  house_domain later; no migration required.
- Add an explicit M&A section: existing redirect variants (House Redirect,
  Authoritative Location Redirect) handle reorganizations; resolution follows
  redirects through house_domain.
- Add a clear field-resolution table: where each consumer-side question is
  answered. No inheritance/override, just per-brand vs per-house ownership.

Trust model still single-hop, mutual-assertion via house_domain ↔ brand_refs[].
Migration still pull-based. brands[] still first-class (not deprecated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concrete schema additions for review against RFC #3533. Additive only —
existing publishers unchanged.

brand.json schema:
- New variant: Brand Canonical Document. Self-published per-brand doc
  with `parent_house: BrandRef` pointer + optional house_attributes_overrides.
  Composes the existing `brand` definition via allOf so identity fields
  match the inline `brands[]` shape.
- House Portfolio variant gains `brand_refs[]` (pointer brands, child-owned
  data) and `house_attributes` (house-wide inheritable attributes). Required
  changed from ["house","brands"] to ["house"] with anyOf at-least-one of
  brands[]/brand_refs[].
- Two new examples illustrating mixed inline+pointer house and a self-
  published Converse canonical document.

docs/brand-protocol/brand-json.mdx: added a "Proposed (RFC)" callout
pointing at the RFC and PR #3533. Existing four variants documented as-is.

Cross-array invariant — a brand_id MUST NOT appear in both brands[] and
brand_refs[] of the same house — is documented in field descriptions.
JSON Schema can't express it; lint/validator follow-up needed if RFC ratifies.

Status: review-only. Not normative until RFC ratifies. Marked DRAFT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arent_house example

Code-reviewer + protocol-expert findings on PR #3764:

- Brand Canonical Document variant lacked oneOf disambiguation.
  brand definition has additionalProperties: true and the inner allOf
  member had no constraint, so a malformed Portfolio with a stray
  parent_house could silently re-type as a Canonical Document. Replaced
  the narrow not: {required: ["brand_refs"]} with not: {anyOf: [...]}
  blocking all house-only top-level keys (house, brands, brand_refs,
  house_attributes, authorized_operators).
- Fixed example: parent_house: { domain: "nikeinc.com", brand_id: "converse" }
  read as "I'm pointing at converse inside nikeinc.com" — but Converse owns
  this document; the brand_id was the self, not the parent. parent_house
  is a pointer UP, only domain is meaningful for a typical house pointer.
  Updated both the schema example and the docs section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…managed_by + drop house_attributes

Tracks RFC #3533 v3. Replaces the v2 cut with the simplified model decided
during expert review.

Schema deltas (relative to previous impl cut):
- Replace parent_house ($ref core/brand-ref.json) with house_domain (string,
  reuses #/definitions/domain). Strings match the existing House Redirect
  convention and drop the planned core/house-ref.json file entirely.
- Make house_domain optional on Brand Canonical Document so standalone brands
  (Patagonia, Liquid Death) have a valid shape without spinning up a degenerate
  "house of one." If a standalone brand is later acquired, it adds house_domain
  and the new house adds it to brand_refs[]. No new variant needed.
- Drop house_attributes / house_attributes_overrides entirely. Inheritance/
  override semantics turned out muddy — if a brand could weaken a house policy,
  it wasn't really a house policy. House-level fields stay where they already
  are (data_subject_contestation, trademarks, authorized_operators on the
  house schema). Brand-level constraints are additive, not overrides.
- Replace the brand_refs[] $ref to core/brand-ref.json with an inline
  {domain, brand_id?, managed_by?} shape. brand-ref.json's existing
  governance-override fields (industries, data_subject_contestation) don't
  belong on a house-side declaration; they're consumer-side overrides.
- Add managed_by (string, optional) on brand_refs[] entries — house-declared
  delegation for grouping/discovery, non-trust-bearing. Captures WPP/Publicis
  reality (BBH manages this brand for WPP) without reintroducing recursive
  trust.

Examples updated:
- Nike Inc. mixed-shape (inline Nike SB + pointer Converse, Jordan)
- WPP with managed_by (BBH Sport managed_by bbh.com, etc.)
- Converse self-published with house_domain
- Patagonia standalone (no house_domain)

docs/brand-protocol/brand-json.mdx: rewritten Distributed Extensions section
to match the new shape. Adds the four worked examples. Adds field-resolution
table showing where each consumer-side question is answered (no inheritance,
no overrides). Adds explicit M&A section showing existing redirect variants
handle reorganizations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rmance, strictest-of compliance

Addresses round-2 expert review on RFC v3:

- managed_by gets normative language: MUST NOT be used for trust/auth
  decisions; verifiers MUST ignore for authorization. UIs SHOULD render as
  unilateral house claim, not aggregate cross-house.
- New "Compliance fields: strictest-of resolution" section. Identity fields
  (logos, voice, tone) stay brand-wins. Compliance/governance fields
  (data_subject_contestation, compliance_policies, regulated-category flags)
  resolve as union/strictest of house-level and brand-level — brand cannot
  weaken house assertions, only add stricter constraints.
- New "Standalone brands" subsection: absence of house_domain ⇒ standalone,
  regardless of one-sided third-party brand_refs[] claims.
- New "Conformance" section formalizing brand_id cross-array uniqueness,
  within-array uniqueness, mutual-assertion as canonical trust primitive,
  managed_by non-trust, standalone trumps third-party claim, strictest-of
  rule, 180-day TTL.
- New "Prior art" section citing IAB Tech Lab ads.txt/sellers.json reciprocal
  publication model — same trust shape, deployed industry pattern.

No structural changes to the data model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion + tighten not deny-list

Code-reviewer round-2 nits on PR #3764:

- Extract brand_refs[] item shape from inline to #/definitions/brand_ref
  (named type for SDK codegen / generator output). Description includes the
  managed_by non-trust normative language directly on the field.
- Tighten Brand Canonical Document not.anyOf to also block House Redirect's
  region and note keys. Disambiguation against House Redirect was already
  clean via id+names requirement, but the deny-list now matches all four
  other variants' top-level fields explicitly.

No example or behavior changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d brand-level trademarks

Consolidates #3409 / #3533 (RFC) / #3764 (schema cut) / #3910 (fold) / #3909
(typed trademarks) into one normative PR.

**Fold (#3910):** RFC content (Motivation, Conformance, strictest-of compliance
resolution, Prior art) absorbed into docs/brand-protocol/brand-json.mdx as
the normative spec. Proposals subdir and the standalone RFC file deleted.
Variant list now reads as five variants from the top; Brand Canonical Document
slots in as #5 alongside the other four.

**Typed trademarks (#3909):** New #/definitions/trademark extracts the inline
house-portfolio shape ({registry, number, mark}) with optional status,
license_type, countries. brand definition gains trademarks: Trademark[].
House Portfolio's inline trademarks[] migrated to $ref. House-level + brand-
level resolution is union (both lists are valid claims).

Existing publishers are unaffected — all additions are optional, the existing
four variants are unchanged, and the inline trademark shape continues to
validate against the extracted definition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley added this to the 3.1.0 milestone May 13, 2026
@bokelley bokelley added the needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas label May 13, 2026
…tightening, conformance polish

Folds protocol/product/docs expert review on PR #4505. No design rollbacks;
all changes tightening or expanding.

Schema
- Rename #/definitions/brand_ref → portfolio_entry (disambiguate from
  buyer-side core/brand-ref.json which identifies brands in media plans).
  Make brand_id required on portfolio_entry so cross-array invariant is
  enforceable. Add optional effective_at (ISO 8601) so consumers age edges
  from a publisher-anchored date.
- Brand Canonical Document not.anyOf deny-list adds authoritative_location,
  redirect_reason, redirect_effective_at — closes ambiguous-match holes
  against redirect variants.
- Trademark gains optional licensor_domain (when license_type=licensed_in)
  and nice_classes (Nice Classification for cross-industry disambiguation
  — Delta-airline vs Delta-faucet).

Trust model
- Three-tier trust table: brand-identity (TLS-only) and brand-relationships
  (mutual-assertion-gated) resolve separately. A leaf-only edge keeps
  identity trust; only relationships block.
- Self-healing notification SHOULD: consumers SHOULD email the house's
  contact.email when they encounter a leaf-only edge, so the parent team
  can complete the reciprocal entry. Rate-limited per {leaf, house}.
- managed_by reframed as a directory field (aggregation across houses is
  the intended use); MUST NOT trust kept, SHOULD NOT aggregate dropped —
  the latter was fiction.

Conformance
- New invariants: house_domain MUST NOT appear in brands[]; brand_refs[]
  unique by domain (not just brand_id); mutual-assertion verification MUST
  follow House Redirects on the house side; strictest-of compliance
  expanded to include policy_categories and brand-level disclaimers[];
  edge-aging language reframed around effective_at rather than a fixed
  180-day SHOULD.
- Resolution algorithm: "resolve recursively (single hop)" → "resolve once"
  with explicit clause that the followed document MUST be a Brand Canonical
  Document.

Docs / structure
- Variant 4/5 cross-reference trust model forward instead of asserting it
  inline — variant 5 reads cleanly when first encountered.
- Variant 5 field table fully enumerates top-level fields and explicitly
  lists prohibited fields.
- New "Adopting brand_refs[] for an existing portfolio" subsection
  documenting the migration path and AAO registry behaviour.
- New "Out of scope" subsection: JVs with two parents, PE-opacity rollups,
  jurisdictional governance divergence — explicitly outside brand.json's
  scope (brand identity ≠ corporate legal structure).
- Prior art expanded with app-ads.txt and WebFinger / host-meta (RFC 7033,
  6415) as IETF analogues for well-known + JSON resource discovery.
- Frontmatter description and managed_by prose updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… headline

3.1 doesn't ship today; minor releases accumulate from changesets. This
opens the 3.1 section with PR #4505 as the headline feature so the
narrative space exists when subsequent 3.1 changesets land.

Includes adopter-action table for the publisher-visible behaviour change
(trademark string drift) and links to the four design follow-ups (#4521
verification endpoint, #4522 JV multi-parent, #4523 PE-opacity tradeoff,
#4524 manager-edge reciprocation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-wg-review Blocked on a working-group decision — surface in WG meeting agendas

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant